package com.shiku.imserver.common.http;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tio.core.ChannelContext;
import org.tio.core.exception.AioDecodeException;
import org.tio.core.exception.LengthOverflowException;
import org.tio.core.utils.ByteBufferUtils;
import org.tio.http.common.HttpConfig;
import org.tio.http.common.RequestLine;
import org.tio.http.common.UploadFile;
import org.tio.http.common.utils.HttpParseUtils;
import org.tio.utils.SystemTimer;
import org.tio.utils.hutool.StrUtil;

public class HttpMultiBodyDecoder {
  public static class Header {
    private String contentDisposition = "form-data";
    
    private String name = null;
    
    private String filename = null;
    
    private String contentType = null;
    
    private Map<String, String> map = new HashMap<>();
    
    public String getContentDisposition() {
      return this.contentDisposition;
    }
    
    public String getContentType() {
      return this.contentType;
    }
    
    public String getFilename() {
      return this.filename;
    }
    
    public Map<String, String> getMap() {
      return this.map;
    }
    
    public String getName() {
      return this.name;
    }
    
    public void setContentDisposition(String contentDisposition) {
      this.contentDisposition = contentDisposition;
    }
    
    public void setContentType(String contentType) {
      this.contentType = contentType;
    }
    
    public void setFilename(String filename) {
      this.filename = filename;
    }
    
    public void setMap(Map<String, String> map) {
      this.map = map;
    }
    
    public void setName(String name) {
      this.name = name;
    }
  }
  
  public static interface MultiBodyHeaderKey {
    public static final String Content_Disposition = "Content-Disposition".toLowerCase();
    
    public static final String Content_Type = "Content-Type".toLowerCase();
  }
  
  public enum Step {
    BOUNDARY, HEADER, BODY, END;
  }
  
  private static Logger log = LoggerFactory.getLogger(HttpMultiBodyDecoder.class);
  
  public static void decode(HttpRequest request, RequestLine firstLine, byte[] bodyBytes, String initboundary, ChannelContext channelContext, HttpConfig httpConfig) throws AioDecodeException {
    if (StrUtil.isBlank(initboundary))
      throw new AioDecodeException("boundary is null"); 
    long start = SystemTimer.currTime;
    ByteBuffer buffer = ByteBuffer.wrap(bodyBytes);
    buffer.position(0);
    String boundary = "--" + initboundary;
    String endBoundary = boundary + "--";
    Step step = Step.BOUNDARY;
    try {
      while (true) {
        if (step == Step.BOUNDARY) {
          String line = ByteBufferUtils.readLine(buffer, request.getCharset(), Integer.valueOf(512));
          if (boundary.equals(line)) {
            step = Step.HEADER;
          } else {
            if (endBoundary.equals(line))
              break; 
            throw new AioDecodeException("line need:" + boundary + ", but is: " + line + "");
          } 
        } 
        Header multiBodyHeader = new Header();
        if (step == Step.HEADER) {
          List<String> lines = new ArrayList<>(2);
          while (true) {
            String line = ByteBufferUtils.readLine(buffer, request.getCharset(), Integer.valueOf(512));
            if ("".equals(line))
              break; 
            lines.add(line);
          } 
          parseHeader(lines, multiBodyHeader, channelContext);
          step = Step.BODY;
        } 
        if (step == Step.BODY) {
          Step newParseStep = parseBody(multiBodyHeader, request, buffer, boundary, endBoundary, channelContext, httpConfig);
          step = newParseStep;
          if (step == Step.END)
            break; 
        } 
      } 
    } catch (LengthOverflowException loe) {
      throw new AioDecodeException(loe);
    } catch (UnsupportedEncodingException e) {
      log.error(channelContext.toString(), e);
    } finally {
      long end = SystemTimer.currTime;
      long iv = end - start;
      log.info("{}ms", Long.valueOf(iv));
    } 
  }
  
  public static Step parseBody(Header header, HttpRequest request, ByteBuffer buffer, String boundary, String endBoundary, ChannelContext channelContext, HttpConfig httpConfig) throws UnsupportedEncodingException, LengthOverflowException, AioDecodeException {
    int initPosition = buffer.position();
    while (buffer.hasRemaining()) {
      String line = ByteBufferUtils.readLine(buffer, request.getCharset(), Integer.valueOf(httpConfig.getMaxLengthOfMultiBody()));
      boolean isEndBoundary = endBoundary.equals(line);
      boolean isBoundary = boundary.equals(line);
      if (isBoundary || isEndBoundary) {
        int startIndex = initPosition;
        int endIndex = buffer.position() - (line.getBytes()).length - 2 - 2;
        int length = endIndex - startIndex;
        byte[] dst = new byte[length];
        System.arraycopy(buffer.array(), startIndex, dst, 0, length);
        String filename = header.getFilename();
        if (filename != null) {
          if (StrUtil.isNotBlank(filename)) {
            UploadFile uploadFile = new UploadFile();
            uploadFile.setName(filename.replaceAll("%", ""));
            uploadFile.setData(dst);
            uploadFile.setSize(dst.length);
            request.addParam(header.getName(), uploadFile);
          } 
        } else {
          request.addParam(header.getName(), new String(dst, request.getCharset()));
        } 
        if (isEndBoundary)
          return Step.END; 
        return Step.HEADER;
      } 
    } 
    log.error("is null");
    throw new AioDecodeException("step is null");
  }
  
  public static void parseHeader(List<String> lines, Header header, ChannelContext channelContext) throws AioDecodeException {
    if (lines == null || lines.size() == 0)
      throw new AioDecodeException("multipart_form_data" ); 
    try {
      for (String line : lines) {
        String[] keyvalue = line.split(":");
        String key = StrUtil.trim(keyvalue[0]).toLowerCase();
        String value = StrUtil.trim(keyvalue[1]);
        header.map.put(key, value);
      } 
      String contentDisposition = (String)header.map.get(MultiBodyHeaderKey.Content_Disposition);
      String name = HttpParseUtils.getSubAttribute(contentDisposition, "name");
      String filename = HttpParseUtils.getSubAttribute(contentDisposition, "filename");
      String contentType = (String)header.map.get(MultiBodyHeaderKey.Content_Type);
      header.setContentDisposition(contentDisposition);
      header.setName(name);
      header.setFilename(filename);
      header.setContentType(contentType);
    } catch (Throwable e) {
      log.error(channelContext.toString(), e);
      throw new AioDecodeException(e.toString());
    } 
  }
}
